JVM

JVM 线程安全与锁优化

Posted by 余腾 on 2019-04-14
Estimated Reading Time 6 Minutes
Words 1.9k In Total
Viewed Times

本篇将介绍线程安全所涉及的概念和分类、同步实现的方式及虚拟机的底层运作原理,虚拟机为实现高效并发所采取的一系列锁优化措施。

一、线程安全

《Java Concurrency In Practice》的作者Brian Goetz对“线程安全”有个比较恰当的定义:
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

这个定义比较严谨,它要求线程安全的代码都必须具备一个特征:

  • 代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。

Java语言中的线程安全

按照线程安全的程度由强至弱分成五类

不可变:外部的可见状态永远不会改变,在多个线程之中永远都是一致的状态。(一定是线程安全的)

  • 如何实现:
    • 如果共享数据是一个基本数据类型,只要在定义时用final关键字修饰;就可以保证它是不可变的。
    • 如果共享数据是一个对象,最简单的方法是把对象中带有状态的变量都声明为final。构造函数结束之后,它就是不可变的。

绝对的线程安全:完全满足之前给出的线程安全的定义,即达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”。

  • 在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
    • java.util.Vector 是一个线程安全的容器。add()、get()、size() 都被sync修饰,尽管效率低,但确实是安全的。
    • 即使它所有方法都被修饰成同步,也不意味着调用它的时候永远都不需要同步手段了。在多线程时还需要加 sync 关键字。

相对的线程安全:能保证对该对象单独的操作是线程安全的,在调用时无需做额外保障措施,但对于一些特定顺序的连续调用,可能需要在调用端使用额外的同步措施来保证调用的正确性。

  • 是通常意义上所讲的线程安全。
  • 大部分的线程安全类都属于这种类型,如Vector、HashTable、Collections、synchronizedCollection()包装的集合。

线程兼容:对象本身非线程安全的,但可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。

  • 是通常意义上所讲的非线程安全。
  • Java API中大部分类都是属于线程兼容的,如 ArrayList 和 HashMap …

线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。


线程安全的实现

可分为两大手段,本篇重点在虚拟机本身

  • 1、通过代码编写实现线程安全
  • 2、通过虚拟机本身实现同步与锁机制

1、互斥同步

👆(Mutual Exclusion&Synchronization)

同步:在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。
互斥:是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。

  • 互斥是因,同步是果;互斥是方法,同步是目的。

属于悲观并发策略,即认为只要不做正确的同步措施就肯定会出现问题,因此无论共享数据是否真的会出现竞争,都要加锁。最大的问题是进行线程阻塞和唤醒所带来的性能问题,也称为阻塞同步(Blocking Synchronization)



2、使用synchronized关键字

👆实现线程安全

原理: 编译后会在同步块的前后分别形成monitorentermonitorexit这两个字节码指令,
并通过一个reference类型的参数来指明要锁定和解锁的对象。若明确指定了对象参数,则取该对象的reference;否则,会根据synchronized修饰的是实例方法还是类方法去取对应的对象实例或Class对象来作为锁对象。


过程: 执行monitorenter指令时先要尝试获取对象的锁。若该对象没被锁定或者已被当前线程获取,那么锁计数器+1;若获取对象锁失败,那当前线程会一直被阻塞等待,直到对象锁被另外一个线程释放为止。


特别注意: synchronized同步块对同一条线程来说是可重入的,不会出现自我锁死的问题;还有,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。



3、使用重入锁ReentrantLock

👆实现线程安全

相同: 用法与synchronized很相似,且都可重入。


不同:

  • 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
    • 而synchronized是非公平的,即在锁被释放时,任何一个等待锁的线程都有机会获得锁。
    • ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数改用公平锁。
  • 锁绑定多个条件:一个ReentrantLock对象可以通过多次调用newCondition()同时绑定多个Condition对象。
    • 而在synchronized中,锁对象的wait()和notify()或notifyAl()只能实现一个隐含的条件,若要和多于一个的条件关联不得不额外地添加一个锁。

选择: 在synchronized能实现需求的情况下,优先考虑使用它来进行同步。改进更偏向于sync。



非阻塞同步(Non-Blocking Synchronization)

基于冲突检测的乐观并发策略,即先进行操作,若无其他线程争用共享数据,操作成功;反之产生了冲突再去采取其他的补偿措施。

为了保证操作和冲突检测这两步具备原子性,需要用到硬件指令集,比如:

  • 测试并设置(Test-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap,CAS)
  • 加载链接/条件存储(Load-Linked/Store-Conditional,LL/SC)

无同步方案

定义:不用同步的方式保证线程安全,因为有些代码天生就是线程安全的。下面举两个例子:

  • 可重入代码(Reentrant Code)/纯代码(Pure Code)
  • 线程本地存储(Thread Local Storage)


二、锁优化

解决并发的正确性之后,为了能在线程之间更『高效』地共享数据、解决竞争问题、提高程序的执行效率。下面介绍五种锁优化技术:

  • 适应性自旋(Adaptive Spinning)
  • 锁消除(Lock Elimination)
  • 锁粗化(Lock Coarsening)
  • 轻量级锁(Lightweight Locking)
  • 偏向锁(Biased Locking)

参考

锁优化参考 友链
要点提炼| 理解JVM之线程安全&锁优化

感谢阅读


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !